# Arduino Sketch Generation Script -- See the README
# Copyright (C) 2010  Marcus Wanner
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# The code generated by this program is in the public domain.

######MESSY CODE WARNING#######
###PROCEED AT YOUR OWN RISK!###

#Here's a map of the basic flow to help you on your way:
#Load dict ("specs") from input file
#Do a bit of preprocessing on specs:
#   - Set defaults
#   - Do a bit of reformatting
#   - Digest some parts
#Write headers:
#   - Init common vars
#   - Init vars used by generators and modes
#   - memreset() function
#Write setup() function
#   - Mode byte processing
#   - Individual generator sections
#   - Other stuff for each loop including delay
#Write onRead and onWrite functions

import sys, string
from enum import *

def setdefault(dictionary, key, contents):
    try:
        dictionary[key]
    except KeyError:
        if contents == ValueError:
            raise ValueError, "Field %s is mandatory!" % key
        dictionary[key] = contents
    return dictionary

def out(string):
    outfile.write(string)

infile = sys.argv[1]
exec('from ' + string.split(infile, '.')[0] + ' import specs')
#All persistent variables are held inside specs.
#Working variables may be reused in different sections.
#We proceed to setting defaults and reformatting inside specs
specs = setdefault(specs, 'addr', 0x02)
specs = setdefault(specs, 'ident', {})
defaultident = {
    'version': 'V0.1',
    'maker': 'sktchgen',
    'type': 'Arduino'}
for field in defaultident:
    specs['ident'] = setdefault(specs['ident'], field, defaultident[field])
specs = setdefault(specs, 'memory', [0 for i in range(256)])
fields = ['version', 'maker', 'type']
for fieldnum in range(3):
    for charnum in range(8):
        field = specs['ident'][fields[fieldnum]]
        field = field + '\x00'*(8-len(field))
        specs['memory'][fieldnum*8+charnum] = ord(field[charnum])
specs = setdefault(specs, 'generators', {})
specs['generators'] = setdefault(specs['generators'], TICKDELAY, 100)
specs['gengroups'] = {}
for name in specs['generators']:
    if isinstance(name, str):
        specs['generators'][name] = setdefault(specs['generators'][name], 'addr', ValueError)
        specs['generators'][name] = setdefault(specs['generators'][name], 'length', 1)
        specs['generators'][name] = setdefault(specs['generators'][name], 'contents', ('seq',))
        specs['generators'][name] = setdefault(specs['generators'][name], 'groups', [])
        for group in specs['generators'][name]['groups']:
            try: specs['gengroups'][group].append(name)
            except: specs['gengroups'][group] = [name]
        if specs['generators'][name]['length'] > 16:
            raise ValueError, "Generator length cannot be more than 16!"
specs = setdefault(specs, 'modes', {'none': None})
for addr in specs['modes']:
    if addr == 'none':
        if len(specs['modes']) > 1:
            print ValueError, "Only use mode 'none' if there are no mode controllers!"
        else:
            specs['modes'] = None
        break
    specs['modes'][addr] = setdefault(specs['modes'][addr], 'values', ValueError)
    specs['modes'][addr] = setdefault(specs['modes'][addr], 'length', 1)
    if specs['modes'][addr]['length'] > 4:
        raise ValueError, "Mode length cannot be more than 4!"
specs = setdefault(specs, 'code', {})
for field in ['header', 'setup', 'loop']:
    specs['code'] = setdefault(specs['code'], field, '')

#Arduino requires that we have its source files inside subdirs
import os
filename = string.split(infile, '.')[0]
try: os.mkdir(filename)
except OSError: pass #dir already exists
outfile = open(filename+'/'+filename+'.pde', 'w')
#From here on we use out() to write something to the outfile
out("""//Automatically generated by sketchgen.py.
//See README for more information.

""")
#Writing the header section
out(specs['code']['header'])
out("""#include <Wire.h>
byte request = 0;
byte command = 0;
unsigned long tmp32 = 0;
unsigned long loopn = 0;
byte tmpreply[16];
""")
if specs['modes'] == None: gendefault = 'true'
else: gendefault = 'false'
for name in specs['generators']:
    if isinstance(name, str):
        #Each generator gets at least two vars
        #This tells whether it's enabled or not
        out('boolean gen%sP = %s;\n' % (name, gendefault))
        #And this tells the length of the data to generate
        out('byte gen%sL = %d;\n' % (name, specs['generators'][name]['length']))
        if specs['generators'][name]['contents'][0] == 'list':
            #If the generator uses a list of values, we need two more vars for it
            #The list of values (converted to ints) and the number of items in the list
            inlist = specs['generators'][name]['contents'][1]
            genlist = []
            for item in inlist:
                if specs['generators'][name]['length'] == 1:
                    if isinstance(item, str):
                        genlist.append(ord(item))
                    elif isinstance(item, int):
                        genlist.append(item)
                    else:
                        genlist.append(int(item))
                else:
                    if isinstance(item, int):
                        genlist.append(item)
                    else:
                        if len(item) < specs['generators'][name]['length']:
                            raise ValueError, "Value %s for generator %s is not long enough." % (str(item), name)
                        itemtmp = 0;
                        for charid in range(specs['generators'][name]['length']):
                            char = item[charid]
                            if isinstance(char, str) and len(char) == 1:
                                char = ord(char)
                            else:
                                try:
                                    char = int(char)
                                except ValueError:
                                    raise ValueError, "'%s' cannot be converted into an int!" % str(char)
                            itemtmp += char*(256**charid)
                        genlist.append(itemtmp)
            #We need to format the list as a C array
            strgenlist = str(genlist)
            strgenlist = string.replace(strgenlist, '[', '{')
            strgenlist = string.replace(strgenlist, ']', '}')
            out('long gen%sI[] = %s;\n' % (name, strgenlist))
            out('byte gen%sIL = %d;\n' % (name, len(genlist)))
out('byte memory[256];\n')
out('byte defaultmem[256] = {\n')
nextbyte = 0
for i in range(8):
    line = '  '
    for i in range(32):
        line += str(specs['memory'][nextbyte])+','
        nextbyte += 1
    out(line+'\n')
out('  };\n\n')
#This function resets all the registers in the sensor
out("""void resetmemory(){
  for (int i = 0; i<256; ++i) {
    memory[i] = defaultmem[i];
  }
}

""")

#This section is run once at the start.
#It contains code to be run, not initializations as in the header section
out("void setup() {\n")
out(specs['code']['setup'])
out("  Serial.begin(115200);\n")
out("  Wire.begin(%d);\n" % (specs['addr']/2))
out("""  Wire.onReceive(onWrite);
  Wire.onRequest(onRead);
  resetmemory();
}

""")

#This bit is run every once every TICKDELAY ms.
out("void loop() {\n")
if specs['modes'] != None:
    #First we turn all generators off
    out('  //All gens false\n')
    for name in specs['generators']:
        if isinstance(name, str):
            out('  gen%sP = false;\n' % name)
    #Then each generator gets its own switch() construct
    for addr in specs['modes']:
        out('  //Mode address 0x%x\n' % addr)
        length = specs['modes'][addr]['length']
        if length > 1:
            valuestr = 'memory[%d]' % addr
            for i in range(1, length):
                valuestr += "+memory[%d]*%d" % (addr+i, 256**i)
            out('  unsigned long tmp%d = %s;\n' % (addr, valuestr))
            out('  switch (tmp%d) {\n' % addr)
        else:
            out('  switch (memory[%d]) {\n' % addr)
        values = list(specs['modes'][addr]['values'].iterkeys())
        values.sort() #put "other" at the end
        for val in values:
            if isinstance(val, int):
                out("    case %d:\n" % val)
            elif val == "other":
                out("    default:\n")
            for option in specs['modes'][addr]['values'][val]:
                target = option[0]
                if isinstance(target, str):
                    genson = str(bool(option[1])).lower()
                    out('      //%s %s\n' % (target, genson))
                    out('      gen%sP = %s;\n' % (target, genson))
                elif isinstance(target, int): #if it's an enumerated code
                    if target == ALLGENS:
                        genson = str(bool(option[1])).lower()
                        out('      //All %s\n' % genson)
                        for name in specs['generators']:
                            if isinstance(name, str):
                                out('      gen%sP = %s;\n' % (name, genson))
                    elif target == GROUP:
                        group, genson = option[1:3]
                        genson = str(bool(genson)).lower()
                        out('      //Group %s %s\n' % (group, genson))
                        for name in specs['gengroups'][group]:
                            out('      gen%sP = %s;\n' % (name, genson))
                    elif target == MEMRESET:
                        out("      resetmemory();\n")
                        out("      loopn = 0;\n")
                        out("      return;\n")
                    elif target == CODE:
                        out('      //Inserted code\n')
                        out(option[1])
                    else:
                        raise ValueError, "Mode address %d value %d invalid enumerated option %d" % (addr, val, target)
                else:
                    raise ValueError, "Mode address %d value %d invalid option %s" % (addr, val, str(target))
            out("      break;\n")
        out("  }\n")
else:
    out("  //No mode controllers\n")

for gen in specs['generators']:
    if isinstance(gen, str):
        out("  //Generator %s\n" % gen)
        out("  if (gen%sP) {\n" % gen) #if the generator is enabled
        params = specs['generators'][gen]
        #this if-elif block determines the expression which is evaluated to yeild
        #the value for the generator's register(s), "valstr"
        if params['contents'][0] == 'static':
            valstr = str(params['contents'][1]) #It's a literal int
        elif params['contents'][0] == 'seq':
            if len(params['contents']) > 1:
                start, stop, offset = params['contents'][1]
            else:
                start, stop, offset = (0, 256**params['length'], 0)
            valstr = "(loopn+%d)%%%d+%d" % (offset, stop-start, start)
        elif params['contents'][0] == 'list':
            valstr = "gen%sI[loopn%%gen%sIL]" % (gen, gen)
        else:
            raise ValueError, "Invalid data scheme %s for generator %s" % (str(params['contents'][0]), gen)
        if params['length'] == 1:
            out('    memory[0x%x] = %s;\n' % (params['addr'], valstr))
        else:
            #We put the bytes in little-endian order as this seems to be the standard
            out('    tmp32 = %s;\n' % valstr)
            #TODO: Change to a loop only in python?
            out('    for (int i; i<gen%sL; ++i) {\n' % gen)
            out('      memory[i+0x%x] = tmp32 >> (8*i);\n' % params['addr'])
            out('    }\n')
        out("  }\n")
    else:
        if gen != TICKDELAY:
            raise ValueError, "Invalid generator name/option %s" % str(gen)
if len(specs['code']['loop']) > 0:
    out("  //Inserted code\n")
    out(specs['code']['loop'])
out("  delay(%d);\n" % specs['generators'][TICKDELAY])
out("  loopn++;\n")
out("}\n\n")

#These two functions handle communications with the brick
out("""void onWrite(int howmany) {
  if (howmany == 1) {
    request = Wire.receive();
  }
  else if (howmany > 1) {
    command = Wire.receive();
    for (int i=0; i<Wire.available(); ++i) {
      memory[command+i] = Wire.receive();
    }
  }
}\n\n""")
out("""void onRead() {
  for (int i=0; i<16; ++i) {
    tmpreply[i] = memory[request+i];
  }
  Wire.send(tmpreply, 16);
}\n\n""")

outfile.close()